package com.ecoolex.union.pay.retailer.core.util;

import com.ecoolex.framework.common.enums.ResultCode;
import com.ecoolex.framework.common.exception.BizException;
import com.ecoolex.framework.common.exception.ExternalCallException;
import com.ecoolex.framework.common.util.Assert;
import com.ecoolex.framework.common.util.Check;
import com.ecoolex.union.pay.retailer.core.manager.UnionPayCouponCallRecordManager;
import com.ecoolex.union.pay.retailer.core.properties.UnionPayCouponPropertis;
import com.ecoolex.union.pay.retailer.core.util.unionpay.LogUtil;
import com.ecoolex.union.pay.retailer.pojo.UnionPayCouponOperateRequest;
import com.ecoolex.union.pay.retailer.pojo.UnionPayCouponOperateResponse;
import com.google.gson.Gson;
import com.jayway.jsonpath.JsonPath;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.comparator.Comparators;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateCrtKey;
import java.util.Base64;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;

@Service
@EnableConfigurationProperties(UnionPayCouponPropertis.class)
@EnableRetry
public class UnionPayCouponSecureService {

    @Autowired
    Gson gson;

    @Autowired
    RestTemplateBuilder restTemplateBuilder;

    @Autowired
    UnionPayCouponPropertis unionPayCouponPropertis;

    @Autowired
    UnionPayCouponCallRecordManager unionPayCouponCallRecordManager;

    public static final String X509 = "X.509";

    /** 貌似默认是RSA/NONE/PKCS1Padding，未验证 */
    public static final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";

    /**
     * 最大文件加密块
     */
    private static final int MAX_ENCRYPT_BLOCK = 117;

    /**
     * 最大文件解密块
     */
    private static final int MAX_DECRYPT_BLOCK = 256;



    /**
     *  对数据进行签名
     * @param orgin 原文
     * @return 签名值
     */
    public byte[] signData(String orgin) {
        try {
            Resource resource = unionPayCouponPropertis.getSignature().getPrivateKey();
            char[] charPassword = unionPayCouponPropertis.getSignature().getPassword().toCharArray();
            String alias = "";
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(resource.getInputStream(),charPassword);
            Enumeration<String> e = keyStore.aliases();
            if (e.hasMoreElements()){
                alias = (String)e.nextElement();
            }
            RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyStore.getKey(alias, charPassword);
            Signature sign = Signature.getInstance("SHA256WithRSA");
            sign.initSign(privateKey);
            byte[] bysData = orgin.getBytes(StandardCharsets.UTF_8);
            sign.update(bysData);
            byte[] signByte = sign.sign();
            return signByte;
        }catch (Exception e){
            LogUtil.writeErrorLog("签名失败", e);
            throw new BizException(ResultCode.CALL.build(97),"签名失败");
        }
    }

    /**
     * 验证签名
     * @param plainText 原文
     * @param signature 签名
     * @return 是否验签通过
     */
    public boolean verify(String plainText, String signature) {
        try {
            Resource publicKeyResource = unionPayCouponPropertis.getSignature().getPublicKey();
            CertificateFactory certificatefactory = CertificateFactory.getInstance(X509);
            X509Certificate cert = (X509Certificate) certificatefactory.generateCertificate(publicKeyResource.getInputStream());
            PublicKey publicKey = cert.getPublicKey();
            Signature publicSignature  = Signature.getInstance("SHA1withRSA");
            publicSignature.initVerify(publicKey);
            publicSignature.update(plainText.getBytes(StandardCharsets.UTF_8));

            byte[] signatureBytes = Base64.getDecoder().decode(signature);
            return publicSignature.verify(signatureBytes);
        }catch (Exception e){
            LogUtil.writeErrorLog("验签失败", e);
            throw new BizException(ResultCode.CALL.build(98),"验签失败");
        }
    }


    /**
     * 解密数据
     * @param encryptData
     * @return
     */
    String decryptData(String encryptData) {
        try {
            UnionPayCouponPropertis.Encrypt encrypt = unionPayCouponPropertis.getEncrypt();
            Resource publicKeyResource = encrypt.getPublicKey();
            CertificateFactory certificatefactory = CertificateFactory.getInstance(X509);
            X509Certificate cert = (X509Certificate) certificatefactory.generateCertificate(publicKeyResource.getInputStream());
            PublicKey publicKey = cert.getPublicKey();
            Cipher tcsCipher = Cipher.getInstance(publicKey.getAlgorithm());//通过公钥的加密算法获取加解密实例
            tcsCipher.init(Cipher.DECRYPT_MODE, publicKey);//通过公钥初始化实例
            byte[] encode = Base64.getEncoder().encode(encryptData.getBytes());/* 要加密的字符串进行编码 */
            byte[] doFinal = tcsCipher.doFinal(encode);/*加密*/
            String tcsEncryptPassword = Base64.getEncoder().encodeToString(doFinal);/* 将加密后的字符串进行编码 */
            return  tcsEncryptPassword;
        }catch (Exception e){
            LogUtil.writeErrorLog("解密失败", e);
            throw new BizException(ResultCode.CALL.build(98),"解密失败");
        }
    }


    /**
     * 加密数据
     * @param orgin
     * @return
     */
    String encryptData(String orgin) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            UnionPayCouponPropertis.Encrypt encrypt = unionPayCouponPropertis.getEncrypt();
            CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509");
            Certificate cert = certificatefactory.generateCertificate(encrypt.getPublicKey().getInputStream());
            PublicKey publicKey = cert.getPublicKey();

            Cipher tcsCipher = Cipher.getInstance(publicKey.getAlgorithm());//通过公钥的加密算法获取加解密实例

            tcsCipher.init(Cipher.ENCRYPT_MODE, publicKey);//通过公钥初始化实例
            byte[] bytes = orgin.getBytes();
            int inputLen = orgin.length();
            int offSet = 0;
            byte[] cache;
            int i = 0;
            // 对数据分段加密
            while (inputLen - offSet > 0) {
                if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                    cache = tcsCipher.doFinal(bytes, offSet, MAX_ENCRYPT_BLOCK);
                } else {
                    cache = tcsCipher.doFinal(bytes, offSet, inputLen - offSet);
                }
                out.write(cache, 0, cache.length);
                i++;
                offSet = i * MAX_ENCRYPT_BLOCK;
            }
            byte[] decryptedData = out.toByteArray();
            String encryptedData = Base64.getEncoder().encodeToString(decryptedData);/* 要加密的字符串进行编码 */
            return encryptedData;
        } catch (IOException | CertificateException
                | NoSuchAlgorithmException | InvalidKeyException
                | NoSuchPaddingException | BadPaddingException
                | IllegalBlockSizeException e) {
            LogUtil.writeErrorLog("加密失败", e);
            throw new BizException(ResultCode.CALL.build(98),"加密失败");
        }
    }

    /**
     * 调用银联优惠券数据
     * 异常定义：
     * 301	无效商户
     * 302	签名错误
     * 303	找不到原始交易
     * 304	请求报文格式不正确
     * 305	系统拒绝
     * 306	超时
     * 307	超过优惠券使用次数
     * 308	优惠券不存在
     * 309	不符合优惠券使用规则
     * 310	优惠活动已结束
     * 311	优惠券已过期
     * 399	其他异常
     * @param request
     *
     */
    @Retryable(maxAttempts = 3,value = ResourceAccessException.class)
    public Optional<UnionPayCouponOperateResponse> callUnionPayCouponService(UnionPayCouponOperateRequest request){
        String orginJson = "", encryptedJson = "", response = "", orgin = "";
        RestTemplate restTemplate = restTemplateBuilder.build();
        Boolean isSuccess = false;
        try {
            Assert.isTrue(Check.notNullOrEmpty(request.getTransAmt()), ResultCode.VERIFY.build(1), "交易金额不能为空");
            Assert.isTrue(Check.notNullOrEmpty(request.getTransCurr()), ResultCode.VERIFY.build(1), "交易币种不能为空");

            String cardNo = encryptData(request.getCardNo());

            request.setCardNo(cardNo);

            PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
            mapper.from(unionPayCouponPropertis::getSignCertId).to(request::setSignCertId);
            mapper.from(unionPayCouponPropertis::getEncCertId).to(request::setEncCertId);
            mapper.from(unionPayCouponPropertis::getAcqInsCode).to(request::setAcqInsCode);
            mapper.from(unionPayCouponPropertis::getMerCode).to(request::setMerCode);
            mapper.from(unionPayCouponPropertis::getTermId).to(request::setTermId);

            orginJson = gson.toJson(request);

            LogUtil.writeLog(String.format("请求原文:%s", orginJson));

            String signature = Base64.getEncoder().encodeToString(signData(orginJson));

            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

            String encodeToString = Base64.getEncoder().encodeToString(orginJson.getBytes());

            params.add("data",encodeToString);

            params.add("signature",signature);

            MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
            encryptedJson = gson.toJson(params);
            LogUtil.writeLog(String.format("请求数据:%s", params.toSingleValueMap()));
            headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
            HttpEntity httpEntity = new HttpEntity(params, headers);
            ResponseEntity<String> responseEntity = restTemplate.postForEntity(unionPayCouponPropertis.getEndpoint(), httpEntity, String.class);
            response = responseEntity.getBody();

            LogUtil.writeLog(String.format("返回数据:%s", response));
            String data = JsonPath.read(response, "$.data");

            if(Check.notNullOrEmpty(data)){
                data =  URLDecoder.decode(data, StandardCharsets.UTF_8.displayName());
                orgin = new String(Base64.getDecoder().decode(data));
            }
            LogUtil.writeLog(String.format("返回data字段数据（BASE64 decode后）:%s", orgin));

            UnionPayCouponOperateResponse unionPayCouponOperateResponse =  gson.fromJson(orgin, UnionPayCouponOperateResponse.class);
            if(!"00".equals(unionPayCouponOperateResponse.getRespCode())){
                switch (unionPayCouponOperateResponse.getRespCode()){
                    case "03":throw new ExternalCallException(ResultCode.CALL.build(1),"无效商户");
                    case "11":throw new ExternalCallException(ResultCode.CALL.build(2),"签名错误");
                    case "12":throw new ExternalCallException(ResultCode.CALL.build(3),"找不到原始交易");
                    case "30":throw new ExternalCallException(ResultCode.CALL.build(4),"请求报文格式不正确");
                    case "96":throw new ExternalCallException(ResultCode.CALL.build(5),"系统拒绝");
                    case "98":throw new ExternalCallException(ResultCode.CALL.build(6),"超时");
                    case "M1":throw new ExternalCallException(ResultCode.CALL.build(7),"超过优惠券使用次数");
                    case "M2":throw new ExternalCallException(ResultCode.CALL.build(8),"优惠券不存在");
                    case "M3":throw new ExternalCallException(ResultCode.CALL.build(9),"不符合优惠券使用规则");
                    case "M4":throw new ExternalCallException(ResultCode.CALL.build(10),"优惠活动已结束");
                    case "M5":throw new ExternalCallException(ResultCode.CALL.build(11),"优惠券已过期");
                    default: throw new ExternalCallException(ResultCode.CALL.build(99),"其他异常");
                }
            }else {
                isSuccess = true;
            }
            return Optional.ofNullable(unionPayCouponOperateResponse);
        } catch (ExternalCallException e) {
            LogUtil.writeErrorLog(e.getMessage(), e);
            throw e;
        } catch (BizException e) {
            LogUtil.writeErrorLog(e.getMessage(), e);
            throw e;
        } catch (Exception e) {
            LogUtil.writeErrorLog("调用银联优惠券接口", e);
            throw new BizException(ResultCode.CALL.build(99), "其他异常");
        }finally {
            //写入接口调用记录
            unionPayCouponCallRecordManager.save(request,orginJson,encryptedJson,response,orgin,"systemUser", isSuccess);
        }

    }

    public String build(Map<String, String> params) {
        Iterator<String> sorted = params.keySet().stream().filter(name -> "sign".endsWith(name)).sorted(Comparators.comparable())
                .iterator();
        StringBuilder stringBuilder = new StringBuilder();
        while (sorted.hasNext()) {
            String name = sorted.next();
            stringBuilder.append(name).append("=").append(params.get(name));
            if (sorted.hasNext()) {
                stringBuilder.append("&");
            }
        }
        return stringBuilder.toString();
    }
}
